
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.EOFException;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.net.DatagramPacket;
import java.net.InetAddress;
import java.net.MulticastSocket;
import java.net.Socket;
import java.rmi.AlreadyBoundException;
import java.rmi.Naming;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.UnicastRemoteObject;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;

public class RMI_ServerService extends UnicastRemoteObject implements RMI_ServerInterface
{
    private String directory;
    private long capacity;
    private long used;
    private HashMap<String,RMI_ClientInterface> clients;
    private HashMap<String,RepFile> files;
    private ArrayList<String> transferringFiles;
    private HashMap<String,ArrayList<String>> readCopies;
    private HashMap<String,ArrayList<String>> editCopies;
    
    public RMI_ServerService(String directory, long capacity) throws RemoteException
    {
        this.clients = new HashMap<>();
        this.files = new HashMap<>();
        this.transferringFiles = new ArrayList<>();
        this.editCopies = new HashMap<>();
        this.readCopies = new HashMap<>();
        this.directory = directory;
        this.capacity = capacity;
        this.used = 0;
        loadFiles();
    }
    
    private synchronized void loadFiles()
    {
        ObjectInputStream in;
        
        try
        {
            File file = new File(directory + Constants.FILES_FILE);
            
            if (file.exists())
            {
                in = new ObjectInputStream(new FileInputStream(file));
                
                try
                {
                    while(true)
                    {
                        RepFile rf = (RepFile)in.readObject();
                        files.put(rf.getName(),rf);
                        used += rf.getLength();
                    }
                }
                catch(EOFException e){}
                finally
                {
                    in.close();
                }
            }
        }
        catch(IOException | ClassNotFoundException e){System.out.println("Exception loading files file: " + e);}
    }
    private synchronized void saveFiles()
    {
        ObjectOutputStream out;
        
        File file = new File(directory + Constants.FILES_FILE);
        try 
        {
            out = new ObjectOutputStream(new FileOutputStream(file));
            
            for(RepFile rf: files.values())
            {
                out.writeObject(rf);
                out.flush();
            }
            out.close();
        } 
        catch (IOException ex){System.out.println("Exception saving into files file: " + ex);}
    }
    private synchronized boolean addClient(RMI_ClientInterface client, String user) throws RemoteException
    {
        if (clients.containsKey(user))
        {
            return false;
        }
        clients.put(user,client);
        System.out.println("Cliente aceite: " + client.getIP());
        return true;
    }
    private synchronized void removeClient(String user) throws RemoteException
    {
        System.out.println("Cliente com " + clients.get(user).getIP()+ " desligou-se!");
        clients.remove(user);
        editCopies.remove(user);
        readCopies.remove(user);
        update();
    }
    public synchronized void addFile(String fileName, String author) throws RemoteException
    {
        File f = new File(directory + fileName);
        files.put(fileName,new RepFile(fileName, author, f.length()));
        transferringFiles.remove(fileName);
        used += f.length();
        saveFiles();
    }
    public synchronized void updateFile(String fileName) throws RemoteException 
    {
        File f = new File(directory + fileName);

        used += f.length() - files.get(fileName).getLength();
        files.get(fileName).setLastModified(f.lastModified());
        files.get(fileName).setSize(f.length());
        fileUpdated(fileName);
        saveFiles();
    }
    private synchronized void fileDeleted(String author, String fileName) throws RemoteException
    {
        for (String user: readCopies.keySet())
        {
            if (readCopies.get(user).contains(fileName))
            {
                discardReadCopy(user, fileName);
                clients.get(user).fileDeleted(fileName);
            }
        }
        if (editCopies.containsKey(author) && editCopies.get(author).contains(fileName))
        {
            discardEditCopy(author, fileName);
            clients.get(author).fileDeleted(fileName);
        }
    }
    private synchronized void fileUpdated(String fileName) throws RemoteException
    {
        for (String user: readCopies.keySet())
        {
            if (readCopies.get(user).contains(fileName))
            {
                clients.get(user).fileUpdated(fileName);
            }
        }
    }
    public synchronized RepFile getRepfile(String fileName)
    {        
        return files.get(fileName);
    }
     public synchronized void readCopydownloaded(String user, String fileName)
    {        
        if (readCopies.containsKey(user))
        {
            readCopies.get(user).add(fileName);
            return;
        }
        ArrayList<String> array = new ArrayList<>();
        array.add(fileName);
        readCopies.put(user,array);
    }
    public synchronized void editCopydownloaded(String user, String fileName)
    {        
        if (editCopies.containsKey(user))
        {
            editCopies.get(user).add(fileName);
            return;
        }
        ArrayList<String> array = new ArrayList<>();
        array.add(fileName);
        editCopies.put(user,array);
    }
    public synchronized boolean spaceExists(String fileName, long fileSize)
    {
        if (fileName != null)
        {
            if ((used + fileSize - files.get(fileName).getLength()) < capacity)
            {
                return true;
            }
            return false;
        }
        if (used + fileSize < capacity)
        {
            return true;
        }
        return false;
    }
    public synchronized boolean fileExists(String fileName)
    {
        return files.containsKey(fileName) || transferringFiles.contains(fileName);
    }
    public synchronized void addTransferringFile(String fileName)
    {
        transferringFiles.add(fileName);
    }
    public synchronized void update() throws RemoteException
    {
        for(RMI_ClientInterface client: clients.values())
        {
            client.update(new ArrayList<>(files.values()),new ArrayList<>(clients.keySet()),editCopies,readCopies);
        }
    }
    @Override
    public synchronized void registerClient(RMI_ClientInterface client, String username, String password) throws RemoteException 
    {
        ObjectInputStream in;
        ObjectOutputStream out;
        boolean found = false;
        Account acc;
        
        try
        {
            File file = new File(directory + Constants.ACCOUNTS_FILE);
            
            if (file.exists())
            {
                in = new ObjectInputStream(new FileInputStream(file));
                
                try
                {
                    while(true)
                    {
                        acc = (Account)in.readObject();
                        if (acc.getUser().equals(username))
                        {
                            found = true;
                            break;
                        }
                    }
                }
                catch(EOFException e){}
                in.close();
                if(found) //username já existe
                {
                    client.postAuthenticationError(Constants.ALREADY_EXISTS_ACCOUNT + username);
                    return;
                }
                out = new AppendingObjectOutputStream(new FileOutputStream(file,true));
            }
            else
            {
                out = new ObjectOutputStream(new FileOutputStream(file));
            }
            
            out.writeObject(new Account(username,password));
            out.flush();
            out.close();
            
            client.setUsername(username);
            addClient(client, username);
            client.logged(username);
            update();
        }
        catch(IOException | ClassNotFoundException e)
        {
            System.out.println("Exception using accounts file: " + e);
        }
    }
    @Override
    public synchronized void loginClient(RMI_ClientInterface client, String username, String password) throws RemoteException 
    {
        ObjectInputStream in;
        boolean found = false;
        Account acc = null;
        
        try
        {
            File file = new File(directory + Constants.ACCOUNTS_FILE);
            
            if (file.exists())
            {
                in = new ObjectInputStream(new FileInputStream(file));
                try
                {
                    while(true)
                    {
                        acc = (Account)in.readObject();
                        if (acc.getUser().equals(username))
                        {
                            found = true;
                            break;
                        }
                    }
                }
                catch(EOFException e){}
                in.close();
                if(found) //username existe
                {
                    if (acc.getPass().equals(password))
                    {
                        if (addClient(client, username))
                        {
                            client.setUsername(username);
                            client.logged(username);
                            update();
                            return;
                        }
                        client.postAuthenticationError(Constants.ALREADY_LOGGED);
                        return;
                    }
                    client.postAuthenticationError(Constants.INCORRECT_PASSWORD);
                    return;
                }
            }                       
            client.postAuthenticationError(Constants.NONEXISTENT_ACCOUNT);
        }
        catch(IOException | ClassNotFoundException e){System.out.println("Exception using files: " + e);}
    }
    @Override
    public synchronized void logout(String user) throws RemoteException 
    {
        if (user != null)
        {
            clients.get(user).setUsername(null);
            removeClient(user);
            System.out.println("Removido as " + new Date(System.currentTimeMillis()));
        }
    }
    @Override
    public synchronized void uploadFile(RMI_ClientInterface client) throws RemoteException
    {
        new FileUpload(client).start();
    }
    @Override
    public synchronized void deleteFile(String fileName) throws RemoteException 
    {
        String author = files.get(fileName).getAuthor();
        if (new File(directory + fileName).delete())
        {
            fileDeleted(author,fileName);
            used -= files.remove(fileName).getLength();
            saveFiles();
            clients.get(author).postResponse(Constants.DELETED + fileName);
            return;
        }
        clients.get(author).postResponse(Constants.WARNING + fileName + Constants.IS_BEING_USED);
    }
    @Override
    public synchronized void getReadCopy(RMI_ClientInterface client, String fileName) throws RemoteException 
    {
        new FileDownload(client, fileName, Constants.READ_COPY).start();
    }
    @Override
    public synchronized void discardReadCopy(String username, String fileName) throws RemoteException 
    {
        if (readCopies.get(username).size() == 1)
        {
            readCopies.remove(username);
            return;
        }
        readCopies.get(username).remove(fileName);
    }
    @Override
    public synchronized void getEditCopy(RMI_ClientInterface client, String fileName) throws RemoteException 
    {
        new FileDownload(client, fileName, Constants.EDIT_COPY).start();
    }
    @Override
    public synchronized void discardEditCopy(String username, String fileName) throws RemoteException 
    {
        if (editCopies.get(username).size() == 1)
        {
            editCopies.remove(username);
            return;
        }
        editCopies.get(username).remove(fileName);
    }
    
    private class FileDownload extends Thread
    {
        private String fileName;
        private String username;
        private String ip;
        private String type;
        private Socket downloadSocket;
        private ObjectOutputStream oos;
        private byte[] bytearray;
        private BufferedInputStream bis;
        private OutputStream os;
        private int bytesRead;
        
        public FileDownload(RMI_ClientInterface client, String fileName, String type) throws RemoteException
        {
            this.fileName = fileName;
            this.type = type;
            this.username = client.getUsername();
            this.ip = client.getIP();
        }

        @Override
        public void run() 
        {
            try
            {
                System.out.println("1");
                downloadSocket = new Socket(ip, Constants.TCP_PORT_FILE_DOWNLOAD);
                System.out.println("2");
                os = downloadSocket.getOutputStream();
                System.out.println("3");
                oos = new ObjectOutputStream(os);
                System.out.println("4");
                
                oos.writeObject(getRepfile(fileName));
                oos.flush();
                System.out.println("5");
                oos.writeObject(type);
                oos.flush();
                System.out.println("6");
                bis = new BufferedInputStream(new FileInputStream(directory + fileName));
                System.out.println("7");
                do
                {
                    bytearray = new byte[Constants.TRANSFER_SIZE];
                    bytesRead = bis.read(bytearray,0,bytearray.length);
                    //System.out.println(bytesRead);
                    if (bytesRead != -1)
                    {
                        os.write(bytearray,0,bytesRead);
                        os.flush();
                    }
                }
                while(bytesRead == Constants.TRANSFER_SIZE);
                System.out.println("Transferência terminada as " + new Date(System.currentTimeMillis()));
                
                if (type.equals(Constants.READ_COPY))
                {
                    readCopydownloaded(username,fileName);
                }
                else
                {
                    editCopydownloaded(username,fileName);
                }
                update();
            }
            catch(Exception ex){System.out.println("Em FILE_DOWNLOAD na CLIENT_THREAD: as " + new Date(System.currentTimeMillis()) + " " + ex);}
            finally
            {
                try
                {                    
                    bis.close();
                    oos.close();
                    os.close();
                }
                catch(Exception e){System.out.println(e);}
            }
            System.out.println("Thread fechada as " + new Date(System.currentTimeMillis()));
        }        
    }
    private class FileUpload extends Thread
    {
        private Socket uploadSocket;
        private String fileName;
        private String author;
        private String ip;
        private RMI_ClientInterface client;
        private Long fileLength;
        private Boolean update;
        private boolean allGood;
        private boolean updateServer;
        private byte[] bytearray;
        private int bytesRead;
        private InputStream is;
        private BufferedOutputStream bos;
        private ObjectOutputStream oos;
        private ObjectInputStream ois;
        private String transferResult;
        
        public FileUpload(RMI_ClientInterface client) throws RemoteException
        {
            this.allGood = true;
            this.updateServer = false;
            this.client = client;
            this.author = client.getUsername();
            this.ip = client.getIP();
        }
        
        @Override
        public void run()
        {
            try
            {
                uploadSocket = new Socket(ip, Constants.TCP_PORT_FILE_UPLOAD);
                is = uploadSocket.getInputStream();
                ois = new ObjectInputStream(is);
                oos = new ObjectOutputStream(uploadSocket.getOutputStream());
                
                fileName = (String)ois.readObject();
                fileLength = (Long)ois.readObject();
                update = (Boolean)ois.readObject();
                
                if (update) //UPDATE
                {
                    if (!spaceExists(fileName,fileLength)) //NAO HA ESPACO
                    {
                        transferResult = Constants.WARNING + Constants.NO_SPACE;
                        allGood = false;
                    }
                }
                else //NEW FILE
                {
                    if (fileExists(fileName)) //FICHEIRO JA EXISTE
                    {
                        transferResult = Constants.WARNING + fileName + Constants.ALREADY_EXISTS_FILE;
                        allGood = false;
                    }
                    else if (!spaceExists(null,fileLength))
                    {
                        transferResult = Constants.WARNING + Constants.NO_SPACE;
                        allGood = false;
                    }
                }
                if (allGood)
                {
                    oos.writeObject(Constants.SUCCESS);
                    oos.flush();
                    
                    bos = new BufferedOutputStream(new FileOutputStream(directory + fileName));
                    if (!update)
                    {
                        addTransferringFile(fileName);
                    }
                    do
                    {
                        bytearray = new byte[Constants.TRANSFER_SIZE];
                        bytesRead = is.read(bytearray,0,bytearray.length);
                        fileLength -= bytesRead;
                        //System.out.println(bytesRead + "    " + fileLength);
                        if (bytesRead != -1)
                        {
                            bos.write(bytearray, 0 , bytesRead);
                            bos.flush();
                        }
                    }
                    while(fileLength != 0); // enquanto nao ler tudo
                    
                    bos.close();
                    
                    if(!update)
                    {
                        addFile(fileName, author);
                        transferResult = Constants.ADDED + fileName;
                    }
                    else
                    {
                        updateFile(fileName);
                        discardEditCopy(author, fileName);
                        transferResult = Constants.UPDATED + fileName;
                    }
                    updateServer = true;
                }
                else
                {
                    oos.writeObject(Constants.INSUCCESS);
                    oos.flush();
                }
            }
            catch(IOException | ClassNotFoundException ex)
            {
                System.out.println("Em FILE_UPLOAD na CLIENT_THREAD: as " + new Date(System.currentTimeMillis()) + " " + ex);
                transferResult = Constants.WARNING + Constants.ERROR + fileName;
            }
            finally
            {
                try
                {
                    client.postResponse(transferResult);
                    
                    System.out.println("Socket fechada às " + new Date(System.currentTimeMillis()));
                    oos.close();
                    ois.close();
                    is.close();
                    uploadSocket.close();
                }
                catch(Exception e){System.out.println(e);}
                
                if (updateServer)
                {
                    try
                    {
                        update();
                    }
                    catch(RemoteException e){}
                }
            }
            System.out.println("Thread fechada as " + new Date(System.currentTimeMillis()));
        }
    }
    private static class ServerUDP extends Thread
    {
        @Override
        public void run()
        {
            MulticastSocket s;
            DatagramPacket packet;
            byte[] data;
            String msg, ip;

            try
            {
                s = new MulticastSocket(Constants.UDP_PORT);
                s.joinGroup(InetAddress.getByName(Constants.UDP_MULTICAST));
                ip = new String(InetAddress.getLocalHost().getHostAddress());

                while(true)
                {
                    System.out.println("Server UDP waiting requests...");
                    data = new byte[Constants.DATAGRAM_LENGHT];
                    packet = new DatagramPacket(data,data.length);
                    s.receive(packet);

                    System.out.println("Recebi pedido de " + packet.getAddress().getHostAddress());

                    msg = new String(packet.getData(),0,packet.getLength());

                    if(msg.equals(Constants.REQUEST))
                    {
                        packet = new DatagramPacket(ip.getBytes(),ip.length(),packet.getAddress(),packet.getPort());
                        s.send(packet);
                    }
                }
            }
            catch(Exception e){System.out.println("UDP: " + e);}
        }
    }
    
    public static void main(String[] args)
    {
        String directory = "C:/Servidor/";
        long capacity = 5*1024*1024*1024; //GB
        String registration;
        RMI_ServerService server;
        
        try
        {
            LocateRegistry.createRegistry(Registry.REGISTRY_PORT);
        }
        catch(RemoteException e){}
        try
        {
            if (args.length == 2)
            {
                directory = args[0];
                capacity = Long.parseLong(args[1]);
            }
            registration = "rmi://localhost/ServerService";
            server = new RMI_ServerService(directory, capacity);
            Naming.bind(registration, server);
            new ServerUDP().start();
            System.out.println("Prima uma tecla para terminar o servico");
            System.in.read();
            Naming.unbind(registration);
            RMI_ServerService.unexportObject(server, true);
        }
        catch(NumberFormatException | AlreadyBoundException | IOException | NotBoundException e){}
    }
}
